from fastapi import FastAPI, HTTPException, Body, Depends
from fastapi.encoders import jsonable_encoder
from passlib.context import CryptContext
from fastapi.security import OAuth2PasswordBearer
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from cryptography.fernet import Fernet

import datetime
import jwt

from db_conn import (
    add_user,
    update_user_password,
    retrieve_user,
    retrieve_user_full,
    get_user,
    delete_user,
    add_parolus,
    delete_parolus,
    delete_all_paroluses,
    retrieve_parolus,
    retrieve_paroluses,
    update_parolus,
    update_user_status,
    retrieve_users,
    check_app_config,
    change_app_config
)
from db_models import (
    UserSchema,
    TokenData,
    ErrorResponseModel,
    ResponseModel,
    ParolusSchema,
    uidSchema,
    UpdateParolusModel,
    PasswordChangeRequestModel,
    PasswordNewRequestModel,
    StatusChangeRequestModel,
    ConfigChangeRequestModel,
)


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

SECRET_KEY = "f7383b6c-63a0-4aa8-9072-8772826ea9c8"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60

origins = [
    "*",
]

middleware = [
    Middleware(CORSMiddleware, allow_origins=origins)
]

app = FastAPI(
    middleware=middleware,
    title="Пар*лус - FastAPI",
    docs_url="/api/docs",
    openapi_url="/api/openapi.json",
    summary="https://gitverse.ru/iamintfriend/parolus",
    version="1.0.0",
    contact={
        "name": "Сreator: iamintfriend",
        "url": "https://gitverse.ru/iamintfriend/parolus",
        "email": "vanuko1679@yandex.ru",
    }
)

# ---------------------------------------------------------------------------------------------->>>>>>>>>
# ---------------------------------------------------------------------------------------------->>>>>>>>>
# ---------------------------------------------------------------------------------------------->>>>>>>>>
def verify_password(plain_password, hashed_password) -> bool:
    """Сравнивает хэш пароля"""
    return pwd_context.verify(plain_password, hashed_password)


def create_access_token(data: dict, expires_delta: datetime.timedelta):
    """Создает JWT-токен для аутентификации"""
    to_encode = data.copy()
    expire = datetime.datetime.utcnow() + expires_delta
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


def generate_fernet_key() -> str:
    """Генерирует уникальный ключ шифрования паролей для пользователя"""
    return Fernet.generate_key().decode()


async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
    """Проверяет пользователя для допуска к эндпоинтам"""
    credentials_exception = HTTPException(
        status_code=401,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        id: str = payload.get("sub")
        if id is None:
            raise credentials_exception
        token_data = TokenData(id=id)
    except jwt.PyJWTError:
        raise credentials_exception
    user = await retrieve_user(token_data.id)
    if user is None:
        raise credentials_exception
    return user


# ---------------------------------------------------------------------------------------------->>>>>>>>>
# ---------------------------------------------------------------------------------------------->>>>>>>>>
# ---------------------------------------------------------------------------------------------->>>>>>>>>
@app.get("/api/", tags=["Root"])
async def read_root():
    """Корневая страница - пустая :( Можете добавить сюда что угодно!"""

    return {"message": "Welcome to this fantastic app!"}


@app.post("/api/login", response_description="Login successful")
async def login(username: str = Body(...), password: str = Body(...)):
    """Основной эндпоинт для аутентификации"""

    user = await get_user(username)

    if not user or not verify_password(password, user["password"]):
        raise HTTPException(
            status_code=401,
            detail="Incorrect username or password"
        )
    access_token_expires = datetime.timedelta(
        minutes=ACCESS_TOKEN_EXPIRE_MINUTES
    )
    access_token = create_access_token(
        data={"sub": str(user["_id"]), "sts": bool(user["recievingShares"])},
        expires_delta=access_token_expires
    )

    return {"access_token": access_token, "token_type": "bearer"}


# ---------------------------------------------------------------------------------------------->>>>>>>>>
# ---------------------------------------------------------------------------------------------->>>>>>>>>
# ---------------------------------------------------------------------------------------------->>>>>>>>>
@app.post("/api/users/add", response_description="User data added into the database")
async def add_user_data(user: UserSchema = Body(...)):
    """Добавить нового пользователя"""

    user = jsonable_encoder(user)
    user['key'] = generate_fernet_key()
    user['recievingShares'] = True
    new_user = await add_user(user)

    return ResponseModel(new_user, "User added successfully.")


@app.get("/api/users/list", response_description="Users retrieved")
async def get_users():
    """Получить список всех пользователей"""

    users = await retrieve_users()

    if users:
        return ResponseModel(users, "Users data retrieved successfully")
    return ResponseModel(users, "Empty list returned")


@app.put("/api/users/change-password", response_description="Password changed successfully")
async def change_user_password(
    password_change_request: PasswordChangeRequestModel,
    current_user: TokenData = Depends(get_current_user)
    ):
    """Сменить пароль (только уже аутентифицированному пользователю)"""

    user_id = str(current_user['id'])
    user = await retrieve_user(user_id)

    if user:
        updated_user = await update_user_password(user_id, password_change_request.new_password)
        if updated_user:
            return ResponseModel(
                "Password changed successfully",
                "Password updated for user with ID: {}".format(user_id)
            )
        raise HTTPException(status_code=500, detail="Failed to update password")
    raise HTTPException(status_code=404, detail="User not found")


@app.put("/api/users/new-password", response_description="Password updated successfully")
async def new_user_password(
    password_change_request: PasswordNewRequestModel,
    ):
    """Сменить пароль любому пользователю (для администраторов)"""

    updating_user_id = password_change_request.updating_user_id
    user = await retrieve_user_full(updating_user_id)

    if user:
        updated_user = await update_user_password(updating_user_id, password_change_request.new_password)
        if updated_user:
            return ResponseModel(
                "New password set successfully",
                "New password set for user with ID: {}".format(updating_user_id)
            )
        raise HTTPException(status_code=500, detail="Failed to set new password")
    raise HTTPException(status_code=404, detail="User not found")


@app.put("/api/users/change-status", response_description="Status changed successfully")
async def change_user_status(
    status_change_request: StatusChangeRequestModel,
    current_user: TokenData = Depends(get_current_user)
    ):
    """Сменить статус recievingShares (для получения паролусов)"""

    user_id = str(current_user['id'])
    user = await retrieve_user(user_id)

    if user:
        updated_user = await update_user_status(user_id, status_change_request.new_status)
        if updated_user:
            return ResponseModel(
                "Status changed successfully",
                "Status updated for user with ID: {}".format(user_id)
            )
        raise HTTPException(status_code=500, detail="Failed to update status")
    raise HTTPException(status_code=404, detail="User not found")


@app.delete("/api/users/delete-self", response_description="User account deleted along with associated paroluses")
async def delete_self_user_account(
    current_user: TokenData = Depends(get_current_user)
    ):
    """Удаление пользователя (только для уже аутентифицированного пользователя)"""

    deleted_user = await delete_user(str(current_user['id']))

    if deleted_user:
        await delete_all_paroluses(str(current_user['id']))

        return ResponseModel(
            "User account with ID: {} and associated paroluses deleted".format(current_user['id']), 
            "User account deleted successfully"
        )
    raise HTTPException(status_code=404, detail="User account doesn't exist")


@app.post("/api/users/delete-one", response_description="User account deleted along with associated paroluses")
async def delete_one_user(deleting_uid: uidSchema = Body(...)):
    """Удаление пользователя (для панели администратора)"""

    deleting_uid = deleting_uid.dict()
    deleted_user = await delete_user(str(deleting_uid['uid']))

    if deleted_user:
        await delete_all_paroluses(str(deleting_uid['uid']))

        return ResponseModel(
            "User account with ID: {} and associated paroluses deleted".format(str(deleting_uid['uid'])), 
            "User account deleted successfully"
        )
    raise HTTPException(status_code=404, detail="User account doesn't exist")


@app.post("/api/paroluses/add", response_description="Parolus added successfully")
async def add_parolus_data(
    parolus: ParolusSchema = Body(...),
    current_user: TokenData = Depends(get_current_user)
    ):
    """Создать новый паролус"""

    parolus_dict = parolus.dict()
    parolus_dict["user_id"] = str(current_user['id'])
    parolus_dict["shared"] = bool(False)
    user = await retrieve_user_full(str(current_user['id']))

    if user:
        hash_key = user['key']
    new_parolus = await add_parolus(parolus_dict, hash_key)

    return ResponseModel(new_parolus, "Parolus added successfully.")


@app.get("/api/paroluses/list", response_description="Paroluses retrieved")
async def get_paroluses(current_user: TokenData = Depends(get_current_user)):
    """Получить список всех паролусов (только для уже аутентифицированного пользователя)"""

    user = await retrieve_user_full(str(current_user['id']))

    if user:
        hash_key = user['key']
    paroluses = await retrieve_paroluses(current_user['id'], hash_key)
    if paroluses:
        return ResponseModel(paroluses, "Paroluses data retrieved successfully")

    return ResponseModel(paroluses, "Empty list returned")


@app.get("/api/paroluses/{id}", response_description="Parolus data retrieved")
async def get_parolus_data(id):
    """Получить паролус по ID"""

    parolus = await retrieve_parolus(id)

    if parolus:
        return ResponseModel(parolus, "Parolus data retrieved successfully")

    return ErrorResponseModel("An error occurred.", 404, "Parolus doesn't exist.")


@app.put("/api/paroluses/{id}")
async def update_parolus_data(
    id: str,
    req: UpdateParolusModel = Body(...),
    current_user: TokenData = Depends(get_current_user)
    ):
    """Внести изменения в паролус"""

    parolus_dict = req.dict()
    parolus_dict["user_id"] = str(current_user['id'])

    req = {k: v for k, v in parolus_dict.items() if v is not None}

    user = await retrieve_user_full(str(current_user['id']))
    if user:
        hash_key = user['key']

    updated_parolus = await update_parolus(id, req, hash_key)

    if updated_parolus:
        return ResponseModel(
            "Parolus with ID: {} field update is successful".format(id),
            "Parolus fields updated successfully",
        )
    return ErrorResponseModel(
        "An error occurred",
        404,
        "There was an error updating the parolus data.",
    )


@app.delete("/api/paroluses/{id}", response_description="Parolus data deleted from the database")
async def delete_parolus_data(
    id: str,
    current_user: TokenData = Depends(get_current_user)
    ):
    """Удалить паролус"""

    deleted_parolus = await delete_parolus(id)

    if deleted_parolus:
        return ResponseModel(
            "Parolus with ID: {} removed".format(id), "Parolus deleted successfully"
        )
    return ErrorResponseModel(
        "An error occurred", 404, "Parolus with id {0} doesn't exist".format(id)
    )


@app.post("/api/paroluses/share/{parolus_id}/{user_id}", response_description="Parolus shared successfully")
async def share_parolus(
    parolus_id: str,
    user_id: str,
    current_user: TokenData = Depends(get_current_user)
    ):
    """Поделиться паролусом"""

    parolus = await retrieve_parolus(parolus_id)

    if not parolus:
        raise HTTPException(status_code=404, detail="Parolus not found")

    if parolus["user_id"] != str(current_user['id']):
        raise HTTPException(status_code=403, detail="Forbidden: You are not the owner of this parolus")

    reciever = await retrieve_user_full(user_id)
    reciever_key = reciever['key']

    if not reciever:
        raise HTTPException(status_code=404, detail="Reciever user not found")

    if not reciever["recievingShares"] == True:
        raise HTTPException(status_code=403, detail="Forbidden: User has blocked recieving function.")

    user = await retrieve_user_full(str(current_user['id']))
    user_key = user['key']

    if not user_key:
        raise HTTPException(status_code=500, detail="Failed to retrieve user key")

    cipher_suite = Fernet(user_key)
    decrypted_title = cipher_suite.decrypt(parolus['title'].encode()).decode()
    decrypted_login = cipher_suite.decrypt(parolus['login'].encode()).decode()
    decrypted_password = cipher_suite.decrypt(parolus['password'].encode()).decode()

    shared_parolus = {
        "title": decrypted_title,
        "login": decrypted_login,
        "password": decrypted_password,
        "user_id": user_id,
        "shared": True
    }

    shared_parolus_doc = await add_parolus(shared_parolus, reciever_key)

    return ResponseModel(shared_parolus_doc, "Parolus shared successfully.")


@app.get("/api/app-config", response_description="App config retrieved")
async def get_app_config():
    """Получить конфигурацию приложения"""

    config = await check_app_config()

    if config:
        return ResponseModel(config, "App config retrieved successfully")

    return ErrorResponseModel("An error occurred.", 404, "Parolus doesn't exist.")


@app.put("/api/app-config/change", response_description="Config changed successfully")
async def change_config(
    config_change_request: ConfigChangeRequestModel,
    ):
    """Сменить статус enabled_registration (для панели администратора)"""

    updated_config = await change_app_config(config_change_request.new_config)
    if updated_config:
        return ResponseModel(updated_config, "New status updated")
    else:
        raise HTTPException(status_code=500, detail="Failed to update app config")
